package com.todo.event.activity.application.core.ab;

import cn.hutool.core.util.HashUtil;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.api.client.util.Maps;
import com.todo.common.common.enums.BizErrorCodeEnum;
import com.todo.common.common.exception.BizException;
import com.todo.common.utils.StringUtils;
import com.todo.component.redis.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.*;

/**
 * @author azhebuxing
 * @date 2025/1/8 17:48
 * @description
 */
@Slf4j
@Component
public class ABHelper {

    public static final long REDIS_LOCK_PROCESS_TIME = 2000L;
    public static final long REDIS_LOCK_PROCESS_TIME_5s = 5000L;
    public static final long REDIS_LOCK_PROCESS_TIME_60s = 60000L;
    /**
     * ab测试，活动桶分配数据key
     */
    public static final String ACT_AB_BUCKET_ALLOCATION_KEY = "general:ab:bucket:key:%s";
    /**
     * 桶的数量
     */
    private static final int BUCKET_SIZE = 100;
    /**
     * 允许小数点后的位数
     */
    private static final int TWO_DECIMAL_PLACES = 2;
    /**
     * 扩展的秒数
     */
    private static final int EXTRA_SECONDS = 3600;
    /**
     * 对照组
     */
    private static final String SAMPLE_GROUP = "0";
    /**
     * 实验组
     */
    private static final String EXPERIMENT_GROUP = "1";
    /**
     * 锁key
     */
    private static final String LOCK_KEY = "allocation_lock";

    @Resource
    private RedisService redisHelper;

    /**
     * 检查是否已正常分配
     *
     * @param strategyId 策略ID
     * @return
     */
    public boolean checkAllocated(String strategyId) {
        //构建数据存储的Key
        String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);
        //查询是否已经分配过了
        Boolean exists = redisHelper.hasKey(key);
        return exists != null && exists;
    }

    /**
     * 检查用户是否在实验组中
     *
     * @param strategyId 策略ID
     * @param userId     用户ID
     * @return 返回true表名用户在实验组中，否则就是对照组中
     */
    public Boolean inSampleGroup(String strategyId, Long userId) {
        //参数校验
        if (StringUtils.isBlank(strategyId) || userId == null) {
            throw new BizException(BizErrorCodeEnum.PARAM_ERROR);
        }
        //计算hash
        int hash = HashUtil.fnvHash(strategyId + userId);
        //计算当前用户被分配到的桶的编号
        int bucket = hash % BUCKET_SIZE;

        //构建数据存储的Key
        String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);
        //查询当前桶被分配的分组
        String bucketGroup = redisHelper.hGet(key, String.valueOf(bucket));
        if (bucketGroup == null) {
            log.error("ABHelper#inSampleGroup 活动[{}]的对照组随机桶未初始化完成", strategyId);
            return null;
        }
        log.info("ABHelper#inSampleGroup 用户:{}在活动:{}上执行完成，当前hash值:{}，bucket:{},bucketGroup:{}，结果：{}", userId, strategyId, hash, bucket, bucketGroup, SAMPLE_GROUP.equals(bucketGroup));
        return SAMPLE_GROUP.equals(bucketGroup);
    }

    /**
     * 检查用户是否在实验组中
     *
     * @param strategyId 策略ID
     * @param userId     用户ID
     * @return 返回策略code
     */
    public String inSampleGroupReturnCode(String strategyId, Long userId) {
        //参数校验
        if (StringUtils.isBlank(strategyId) || userId == null) {
            throw new BizException(BizErrorCodeEnum.PARAM_ERROR);
        }
        //计算hash
        int hash = HashUtil.fnvHash(strategyId + userId);
        //计算当前用户被分配到的桶的编号
        int bucket = hash % BUCKET_SIZE;

        //构建数据存储的Key
        String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);
        //查询当前桶被分配的分组
        String bucketGroup = redisHelper.hGet(key, String.valueOf(bucket));
        if (bucketGroup == null) {
            log.error("ABHelper#inSampleGroupReturnCode 活动[{}]的对照组随机桶未初始化完成", strategyId);
            return null;
        }
        log.info("ABHelper#inSampleGroupReturnCode 用户:{}在活动:{}上执行完成，当前hash值:{}，bucket:{},bucketGroup:{}", userId, strategyId, hash, bucket, bucketGroup);
        return bucketGroup;
    }

    /**
     * 给分配活动分配桶数据
     *
     * @param strategyId                 分流策略ID
     * @param sampleGroupRatio           对照组比例,只能是小于1并且包含两位小数的数值，实验组的比例则为1减去对照组的值
     * @param activityRemainValidSeconds 活动剩余的有效秒数
     */
    public void allocateBucket(String strategyId, Double sampleGroupRatio, int activityRemainValidSeconds) {

        if (sampleGroupRatio > 1 || getDecimalPlaces(sampleGroupRatio, TWO_DECIMAL_PLACES)) {
            throw new BizException(BizErrorCodeEnum.PARAM_ERROR, "实验比例设置异常");
        }

        //加锁，防止并发问题
        Boolean lock = redisHelper.tryLock(String.format(ACT_AB_BUCKET_ALLOCATION_KEY, LOCK_KEY + strategyId), 1000);
        if (lock) {
            try {
                //构建数据存储的Key
                String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);

                //查询是否已经分配过了,如果key存在则说明已经分配过，不再分配
                Boolean exists = redisHelper.hasKey(key);
                if (exists != null && exists) {
                    return;
                }

                //进行桶分配
                Map<String, String> allocationResult = randomAllocateBucket(sampleGroupRatio);

                //数据持久化，数据有效期加上1个小时，防止临界时间的问题
                redisHelper.hSetAll(key, allocationResult, activityRemainValidSeconds + EXTRA_SECONDS);
            } finally {
                redisHelper.releaseLock(String.format(ACT_AB_BUCKET_ALLOCATION_KEY, LOCK_KEY + strategyId));
            }
        }
    }

    public static Boolean getDecimalPlaces(double value, int decimalPlaces) {
        String valueString = Double.toString(value); // Convert double to string
        if (valueString.indexOf('.') < 0) {
            return false; // No decimal places if there's no decimal point
        }
        int valuePlace = valueString.length() - valueString.indexOf('.') - 1;
        return valuePlace == decimalPlaces;
    }

    /**
     * 给分配活动分配桶数据
     *
     * @param sampleFlowConfigList       分流详情
     * @param activityRemainValidSeconds 活动剩余的有效秒数
     */
    public void allocateBucket(String strategyId, List<SampleFlowConfig> sampleFlowConfigList, int activityRemainValidSeconds) {

        if (CollectionUtils.isEmpty(sampleFlowConfigList) || !checkFlowConfigRatio(sampleFlowConfigList)) {
            throw new BizException(BizErrorCodeEnum.PARAM_ERROR, "实验比例设置异常");
        }

        //加锁，防止并发问题
        Boolean lock = redisHelper.tryLock(String.format(ACT_AB_BUCKET_ALLOCATION_KEY, LOCK_KEY + strategyId), 1000);
        if (lock) {
            try {
                //构建数据存储的Key
                String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);

                //查询是否已经分配过了,如果key存在则说明已经分配过，不再分配
                Boolean exists = redisHelper.hasKey(key);
                if (exists != null && exists) {
                    return;
                }

                //进行桶分配
                Map<String, String> allocationResult = randomAllocateBucket(sampleFlowConfigList);

                //数据持久化，数据有效期加上1个小时，防止临界时间的问题
                redisHelper.hSetAll(key, allocationResult, activityRemainValidSeconds + EXTRA_SECONDS);
            } finally {
                redisHelper.releaseLock(String.format(ACT_AB_BUCKET_ALLOCATION_KEY, LOCK_KEY + strategyId));
            }
        }
    }

    /**
     * 重新给活动分配桶数据
     *
     * @param strategyId                 分流策略ID
     * @param experimentGroupRatio       实验组比例,只能是小于1并且包含两位小数的数值，对照组的比例则为1减去实验组的值
     * @param activityRemainValidSeconds 活动剩余的有效秒数
     */
    public void reAllocateBucket(String strategyId, Double experimentGroupRatio, int activityRemainValidSeconds) {
        //构建数据存储的Key
        String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);
        //删除之前的分配记录
        redisHelper.del(key);
        allocateBucket(strategyId, experimentGroupRatio, activityRemainValidSeconds);
    }

    /**
     * 重新给活动分配桶数据
     *
     * @param strategyId                 分流策略ID
     * @param sampleFlowConfigList       实验组比例,只能是小于1并且包含两位小数的数值，对照组的比例则为1减去实验组的值
     * @param activityRemainValidSeconds 活动剩余的有效秒数
     */
    public void reAllocateBucket(String strategyId, List<SampleFlowConfig> sampleFlowConfigList, int activityRemainValidSeconds) {
        //构建数据存储的Key
        String key = String.format(ACT_AB_BUCKET_ALLOCATION_KEY, strategyId);
        //删除之前的分配记录
        redisHelper.del(key);
        allocateBucket(strategyId, sampleFlowConfigList, activityRemainValidSeconds);
    }

    /**
     * 随机将100个桶按照比例分配给实验组和对照组
     *
     * @param sampleGroupRatio 实验组比例
     * @return 返回值中Key代表桶的编号，value代表实验组还是对照组，0-对照组，1-实验组
     */
    private Map<String, String> randomAllocateBucket(Double sampleGroupRatio) {

        //实验组桶数量
        int sampleGroupBucketSize = (int) (sampleGroupRatio * BUCKET_SIZE);

        //初始化桶，先将100个桶都放在实验组，然后按比例再随机将桶移动到实验组
        List<String> experimentGroupBuckets = new ArrayList<>(BUCKET_SIZE);
        Set<String> sampleGroupBuckets = new HashSet<>(sampleGroupBucketSize);
        for (int bucket = 0; bucket < BUCKET_SIZE; bucket++) {
            experimentGroupBuckets.add(String.valueOf(bucket));
        }

        //按比例进行分配
        while (sampleGroupBuckets.size() < sampleGroupBucketSize) {
            String bucket = String.valueOf(RandomUtil.randomInt(0, BUCKET_SIZE - 1));
            sampleGroupBuckets.add(bucket);
            experimentGroupBuckets.remove(bucket);
        }

        //构建返回值
        Map<String, String> result = new HashMap<>(128);
        sampleGroupBuckets.forEach(bucket -> result.put(bucket, SAMPLE_GROUP));
        experimentGroupBuckets.forEach(bucket -> result.put(bucket, EXPERIMENT_GROUP));
        return result;
    }

    /**
     * 随机将100个桶按照比例分配给策略组
     *
     * @param sampleFlowConfigList 策略以及比例
     * @return 返回值中Key代表桶的编号，value代表策略id
     */
    private Map<String, String> randomAllocateBucket(List<SampleFlowConfig> sampleFlowConfigList) {

        if (CollectionUtils.isEmpty(sampleFlowConfigList) || !checkFlowConfigRatio(sampleFlowConfigList)) {
            return Maps.newHashMap();
        }

        //初始化桶，先将100个桶都放在当前策略桶中，然后按比例再随机将其他桶移动
        List<String> experimentGroupBuckets = new ArrayList<>(BUCKET_SIZE);
        for (int bucket = 0; bucket < BUCKET_SIZE; bucket++) {
            experimentGroupBuckets.add(String.valueOf(bucket));
        }
        //构建返回值
        Map<String, String> result = new HashMap<>(128);
        for (SampleFlowConfig sampleFlowConfig : sampleFlowConfigList) {
            //实验组桶数量

            int sampleGroupBucketSize = sampleFlowConfig.getFlowRatio();
            Set<String> sampleGroupBuckets = new HashSet<>(sampleGroupBucketSize);

            //按比例进行分配
            Collections.shuffle(experimentGroupBuckets);
            while (sampleGroupBuckets.size() < sampleGroupBucketSize && !experimentGroupBuckets.isEmpty()) {
                sampleGroupBuckets.add(experimentGroupBuckets.get(0));
                experimentGroupBuckets.remove(0);
            }
            sampleGroupBuckets.forEach(bucket -> result.put(bucket, sampleFlowConfig.getFlowStrategyCode()));
        }
        log.info("randomAllocateBucket 随机将100个桶按照比例分配给策略组完成 入参：【{}】出参：【{}】", JSONObject.toJSONString(sampleFlowConfigList), JSONObject.toJSONString(result));
        return result;
    }


    public boolean checkFlowConfigRatio(List<SampleFlowConfig> sampleFlowConfigList) {
        int sum = 0;
        for (SampleFlowConfig sampleFlowConfig : sampleFlowConfigList) {
            sum += sampleFlowConfig.getFlowRatio();
        }
        return sum == 100;
    }
}
